package calao.storage.bdb;

import calao.util.Cursor;
import static calao.util.Logger.*;
import calao.storage.DataBinding;
import calao.storage.Field;
import calao.storage.Record;
import calao.storage.Search;
import calao.storage.Sequence;
import calao.storage.Storage;
import calao.storage.Index;
import calao.storage.Table;
import static calao.storage.DirectAccess.*;

import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.DatabaseException;

public class BdbStorage 
extends Storage 
{

	static File envHome;
	static Environment env;
	static EnvRegistry envRegistry;

	static {
		String path = System.getProperty("calao.bdbEnvPath");
		if (path==null) path = "bdb";
		envHome = new File(path);
	}

	final int id;
	final StorageRegistry registry;

	final HashMap<Table,TableDatabase> tableDbMap = new HashMap<Table,TableDatabase>();
	final HashMap<Index,IndexDatabase> indexDbMap = new HashMap<Index,IndexDatabase>();

	BdbStorage(int id)
	throws DatabaseException
	{
		this.id = id;
		try {
			this.registry = new StorageRegistry(this);
		}
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
		for (Table table : Table.getRegisteredTables()) {
			TableDbRecord rec = registry.getTableDbRecord(table.getName());
			if (rec!=null) checkAndOpen(table, rec);
		}
		log(DEBUG, "Storage %d opened", id);
	}

	private TableDatabase checkAndOpen(Table table, TableDbRecord rec)
	throws DatabaseException
	{
		// TODO compare current table def and recorded table def
		// TODO case when new index appears
		// TODO case when existing index disappears
		TableDatabase db = new TableDatabase(this, rec.dbId, table); 
		tableDbMap.put(table, db);
		for (IndexDatabase indexDB : db.indexDBs) {
			indexDbMap.put(indexDB.index, indexDB);
		}
		return db;
	}

	public int getId()
	{
		return id;
	}

	private int closeDatabases(boolean silent)
	{
		int errorCount = 0;
		for (IndexDatabase indexDB : indexDbMap.values()) {
			try {
				String dbName = indexDB.sdb.getDatabaseName();
				indexDB.sdb.close();
				log(DEBUG, "Index database %s closed", dbName);
			}
			catch (DatabaseException e) {
				if (!silent) throw new RuntimeException(e);
				log(e);
				errorCount++;
			}
		}
		for (TableDatabase tableDB : tableDbMap.values()) {
			try {
				String dbName = tableDB.db.getDatabaseName();
				tableDB.close();
				log(DEBUG, "Table database %s closed", dbName);
			}
			catch (DatabaseException e) {
				if (!silent) throw new RuntimeException(e);
				log(e);
				errorCount++;
			}
		}
		indexDbMap.clear();
		tableDbMap.clear();
		return errorCount;
	}

	@Override
	public synchronized void close() {
		int errorCount = closeDatabases(true);
		try {
			registry.db.close();
		}
		catch (DatabaseException e) {
			log(e);
			errorCount++;
		}
		log(DEBUG, "Storage %d closed (%d)", id, errorCount);
	}

	@Override
	public void drop(Table table, Object... keyValues) 
	{
		try {
			getDatabase(table).drop(keyValues);
		}
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public int getNextValue(Sequence sequence) 
	{
		if (!sequence.isRegistered()) throw new IllegalArgumentException();
		try {
			return registry.getNextValue(sequence);
		} 
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void store(Record record) {
		try {
			getDatabase(record.getTable()).store(record);
		} 
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
		catch (NullPointerException e) {
			// rework exception in case record has wrong values
			for (Field field : record.getTable().getFieldList()) {
				if (field.isMultiValued()) {
					Object[] values = getAll(record, field);
					for (Object value : values) {
						if (value==null) {
							throw new NullPointerException(
									"field "+field.getName()+" value array has null items");
						}
					}
				}
			}
			throw e;
		}
	}

	@Override
	public synchronized Iterable<Table> listTables() {
		return tableDbMap.keySet();
	}

	/** Opens a storage.
	 * 
	 * @param sid the storage unique identifier.
	 */
	public static synchronized BdbStorage open(int sid)
	{
		try {
			if (env==null) {
				EnvironmentConfig config = new EnvironmentConfig();
				config.setAllowCreate(true);
				env = new Environment(envHome, config);
				envRegistry = new EnvRegistry(env);
			}
			BdbStorage storage = new BdbStorage(sid);
			return storage;
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public Cursor<Record> list(Table table) 
	{
		try {
			return getDatabase(table).list();
		}
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public Cursor<Record> listResults(Search search) 
	{
		try {
			return getDatabase(search.getTable()).listResults(search);
		}
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
	}

	synchronized TableDatabase getDatabase(Table table)
	throws DatabaseException
	{
		TableDatabase db = tableDbMap.get(table);
		if (db==null) {
			if (!table.isRegistered()) {
				throw new IllegalArgumentException(
						table.getName()+": table not registered");
			}
			TableDbRecord rec = registry.getTableDbRecord(table.getName());
			if (rec==null) {
				int tableDbId = registry.newTableDbId();
				rec = new TableDbRecord();
				rec.tableDef = table;
				rec.dbId = tableDbId;
				registry.put(rec);
			}
			db = checkAndOpen(table, rec);
		}
		return db;
	}

	public static void setEnvironmentHome(File dir) 
	{
		if (!dir.isDirectory()) throw new IllegalArgumentException();
		envHome = dir;
	}

	public static void closeEnvironment()
	{
		try {
			if (env!=null) {
				envRegistry.close();
				envRegistry = null;
				env.close();
				env = null;
			}
		}
		catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static CursorStats getStats(Cursor cursor)
	{
		try {
			return new CursorStats((TableCursor)cursor);
		}
		catch (ClassCastException e) {
			return null;
		}
	}

	@Override
	public Record fetch(Table table, Object... keyValues) 
	{
		try {
			return getDatabase(table).fetch(keyValues);
		}
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public long countRecords(Table table) 
	{
		try {
			TableDatabase tdb = tableDbMap.get(table);
			return tdb!=null? tdb.db.count() : 0;
		}
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public Cursor<Record> list(Table table, long offset) 
	{
		try {
			return getDatabase(table).list(offset);
		}
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void setValue(Sequence sequence, int value) 
	{
		if (!sequence.isRegistered()) throw new IllegalArgumentException();
		try {
			registry.setValue(sequence, value);
		} 
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public int getValue(Sequence sequence) 
	{
		if (!sequence.isRegistered()) throw new IllegalArgumentException();
		try {
			return registry.getValue(sequence);
		} 
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public void clear(Table table) 
	{
		try {
			getDatabase(table).clear();
		}
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public synchronized void clear()
	{
		closeDatabases(false);
		try {
			for (TableDbRecord rec : registry.getTableDbRecords()) {
				try {
					TableDatabase.removeDatabase(this, rec.dbId);
				}
				catch (DatabaseException e) {
					log(e);
				}
				for (Index index : rec.tableDef.getIndexList()) {
					try {
						if (index!=rec.tableDef.getPrimaryKeyIndex()) {
							IndexDatabase.removeDatabase(this, rec.dbId, index);
						}
					}
					catch (DatabaseException e) {
						log(e);
					}
				}
			}
			registry.clear();
		}
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public int getNumericId(DataBinding binding) 
	{
		try {
			return envRegistry.getId(binding);
		} 
		catch (DatabaseException e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public Table[] getTables() 
	{
		// TODO include *all* storage tables, not just loaded/mapped tables
		// what about tables bound to schemas registered *after* this storage was opened ?
		Collection<Table> tables = tableDbMap.keySet();
		return tables.toArray(new Table[tables.size()]);
	}

}
